纪念2016年这充实的一年

2月

2016年是从2月中旬开始的,【北斗重构】进入上线后反馈阶段,收获了较多的赞赏,也有个别用户反馈UI的短期不适应;

继续负责【北斗】的迭代工作。

3月

组织结构调整已基本确定;

继续跟进【北斗】的迭代及构建优化工作,但预计在5月之前交接出去;

同时接入新的产品【百度指数】,与原来的指数团队沟通产品情况。

4月

完成【北斗】最重要最困难的迭代之一——兴趣节点升级,然后交接工作;

调研指数系产品【品牌数字看板】并在五一前提交框架设计方案。

5月

开发【品牌数字看板】,月底完成基本功能,内部提测;

搬了一次家;

月底终于鼓足勇气开始了英语口语在线外教学习,每周两次晚课,将持续6个月。

6月

【品牌数字看板】根据PM和UE的反馈进行优化,细化外围的产品需求如关于我们、帮助等;

家庭中的造人计划成功,父母开始和我们一起生活,感觉已经离年轻和自由越来越遥远了。

7月

中旬【品牌数字看板】V1.0上线,进入用户测试、培训、反馈阶段;

指数系新产品【涟漪】启动,陆续进行需求沟通、技术调研、制定排期、搭建框架;

【涟漪】完成首页的开发后暂停;

下旬开始准备8月份的述职答辩。

8月

【品牌数字看板】集中优化兼容性问题,于中旬上线;

备战8月14日的述职答辩;

下旬恢复开发【涟漪】;

孕期反应加之工作压力不幸换上甲亢,一个月内暴瘦10斤;

坚持周末去游泳。

9月

【涟漪】提测主要功能,开发外围功能,问题修复及优化;

坚持周末去游泳。

10月

【涟漪】demo版本上线;

天冷了,胎宝宝大了,不去游泳了。

11月

【涟漪】正式版本V1.0上线;

持续6个月的英语口语在线外教学习终于结束了;

天气冷了,胎宝宝也快7个月了,不再骑自行车上班了。

12月

连续两周包括周末加班完成指数系新产品【指数手百版】V1.0、V2.0;

【涟漪】优化,V2.0上线;

买了一套房子,负债累累。

2017年1月

指数系三个产品的工作交接,包括【品牌数字看板】、【涟漪】、【指数手百版】;

计划2月休产假。

前端MVC变形记

背景:

MVC是一种架构设计模式,它通过关注点分离鼓励改进应用程序组织。在过去,MVC被大量用于构建桌面和服务器端应用程序,如今Web应用程序的开发已经越来越向传统应用软件开发靠拢,Web和应用之间的界限也进一步模糊。传统编程语言中的设计模式也在慢慢地融入Web前端开发。由于前端开发的环境特性,在经典MVC模式上也引申出了诸多MV*模式,被实现到各个Javascript框架中都有多少的衍变。在研究MV*模式和各框架的过程中,却是“剪不断、理还乱”:

  1. 为什么每个地方讲的MVC都不太一样?
  2. MVP、MVVM的出现是要解决什么问题?
  3. 为什么有人义正言辞的说“MVC在Web前端开发中根本无法使用”?

带着十万个为什么去翻阅很多资料,但是看起来像view、model、controller、解耦、监听、通知、主动、被动、注册、绑定、渲染等各种术语的排列组合,像汪峰的歌词似的。本篇希望用通俗易懂的方式阐述清楚一些关系,由于接触时间有限,英文阅读能力有限,可能会存在误解,欢迎讨论和纠正。

Read More

有效地勤奋

像博客签名一样”梦想走着走着就丢了,只想充实地过每一天,每天都有挑战“,我一直践行着,脚踏实地。不喜欢计算利益得失寻求技巧的“聪明”孩子或者伪装得很“聪明”的孩子,有些人确实比较聪明,要么是天生的、父辈的勤奋习得传承下来的聪明基因、要么是后天的努力。我爷爷是小学毕业、村里的会计;我爸爸是高中毕业、高考落榜;所以我先天的聪明是有限的,或者说出生于这样的平台之上我已心怀感激,于是剩下的就靠自己了。我一直以勤奋自诩,直到近来发生的一些小事我开始思考如何更有效。

专业书都看过了,我也不是专家

我非常喜欢看书,直到前段时间才正式放弃去国图借书切换到电子阅读平台。偶然间我发现了这样一张图,
两周后你还记得什么
kao,两周后读过的书只剩下10%,难道这是最低效的学习方式?难怪有些书看完之后隔很久再看感觉跟没看过一样。难怪我还不是领域专家。是的,我们都知道遗忘曲线,上学的时候背单词、复习,都是学而时习之,可是工作之后看书谁还会在看完的1、2、4、7、15天再复习一遍。何况在信息充斥的现在,百度一下就知道了,关闭网页就忘记了。而且注意力越发涣、思考越来越浅。还记得以前老师常讲的读书要眼到、口到、笔到、心到,那多累啊。

做了一个月仰卧起坐,我也没有马甲线

我有一个月的时间里,每天200~300个仰卧起坐,体重没有下降、小腹也没有平旦、更不要提马甲线。这对我打击非常大。直到跟一个健身达人聊天才发觉,可能是自己的方法不对。我就像上学时仰卧起坐考试那样、屈腿、仰卧、坐起。达人说不需要完全的坐起、上身起到某一个点上腹部会非常紧绷、吃力,再循环做就好了。我心想,那多累啊。

这两件事,让我开始认真地思考“有效”以及我的座右铭,它像在默默地诉说:你的时间很充足,只管做就好了,总会慢慢地变得熟练、扎实。可现在,我感觉到时间不再充足、想要快一点,有效一点。我们再回顾一下上张图,会发现“我们说过的事”在两周之后也会记得大概80%,可见“说”是非常重要。是时候摒弃“默默地工作”这个信条了,它不仅让你慢慢地忘记做过的事情,也让别人不易觉察到你的付出。“说吧,不要钱。”

jQuery事件系统三

上一节通过给DOM元素添加自定义数据引出jQuery.data这个数据缓存模块,这是数据缓存系统的另一大重头戏,这一节就好好说说jQuery.data。再看jquery源码之前大概大概明确几个要点:

1、jQuery.expando = “jQuery” + ( jQuery.fn.jquery + Math.random() ),即jQuery+版本号+随机数;

2、jQuery.data方法通过参数的不同,可以有几种用法:为DOM元素或Javascript对象设置任意类型的数据,或返回指定名称的数据,或返回关联的数据缓存对象。

3、数据缓存对象上区分内部数据和自定义数据,避免jQuery内部使用的数据和用户自定义的数据发生冲突。
方法._data()设置的数据为内部数据,直接存储在关联的数据存储对象上,如事件缓存数据。
方法.data()设置的数据为自定义数据,存储在关联的数据缓存对象的属性data上;如用户通过data-*自定义数据。

在上节栗子中设置content元素的自定义属性如下,存储位置如下:

 $(".content").data({"tagName":"div","className":"content"});

数据缓存系统

总结一下:上节中通过jQuery._data设置DOM元素的内部属性,事实上是通过jQuery.data实现的,只是传递pvt参数为true(用于内部属性或自定义属性的标识),在jQuery.cache上顺序挂上key值为1、2、3…依此类推的数据缓存对象。同时设置DOM元素本身的属性elem[ jQuery.expando ] = id;以此来关联数据缓存系统。

数据缓存系统

源码如下:

jQuery.extend({
    cache: {},//事件缓存系统
    deletedIds: [],
    uuid: 0,// uuid初始化
    // 生成类似于jQuery18005268001211807132这样的随机数(),避免与用户的自定义属性名冲突
    expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ),
    //jQuery.noData中存放了不支持扩展属性的embed、object、applet元素的节点名称。
    noData: {
        "embed": true,
        "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",//用于检查object元素是否是flash
        "applet": true
    },

    //判断一个DOM元素或javascript对象是否有关联的数据
    hasData: function( elem ) {
        elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];
        return !!elem && !isEmptyDataObject( elem );
    },

    //为DOM元素或javascript对象设置任何类型的数据
    data: function( elem, name, data, pvt /* Internal Use Only */ ) {
        //是否可以附加数据,不可以的话直接返回
        if ( !jQuery.acceptData( elem ) ) {
            return;
        }
        var thisCache, ret,
            //jQuery.expando是一个唯一的字符串,jquery对象产生的时候就生成
            internalKey = jQuery.expando,
            getByName = typeof name === "string",
            // 区分DOM对象和javascript对象,如果是DOM元素,为了避免javascript和DOM元素之间循环引用导致的浏览器(IE6、7)垃圾回收机制不起作用,要把数据存储在全局缓存对象jQuery.cache中;对于Javascript对象,垃圾回收机制自动发生,数据可以之间存储在javascript对象上。
            isNode = elem.nodeType,
            ......
        //如果关联id不存在,则分配一个。
        if ( !id ) {
            if ( isNode ) {
                elem[ internalKey ] = id = jQuery.deletedIds.pop() || ++jQuery.uuid;
            } else {
                id = internalKey;
            }
        }
        //如果数据缓存对象不存在,就初始化为空对象
        if ( !cache[ id ] ) {
            cache[ id ] = {};
            ......
        }
        // 如果参数name是对象或函数,则批量设置数据
        if ( typeof name === "object" || typeof name === "function" ) {
            if ( pvt ) {
                cache[ id ] = jQuery.extend( cache[ id ], name );
            } else {
                cache[ id ].data = jQuery.extend( cache[ id ].data, name );
            }
        }
        thisCache = cache[ id ];
        // 如果参数pvt为true,则设置或读取内部数据,内部数据存储在关联的数据存储对象上;
        // 如果参数pvt为false,则设置或读取自定义数据,自定义数据存储在关联的数据缓存对象的属性data上。
        if ( !pvt ) {
            if ( !thisCache.data ) {
                thisCache.data = {};
            }
            thisCache = thisCache.data;
        }
        //如果参数data不是undefined,则把参数data设置到属性name上。把name统一换成驼峰式。
        if ( data !== undefined ) {
            thisCache[ jQuery.camelCase( name ) ] = data;
        }
        // 参数name为字符串,data为null时读取单个数据。两次读取,使用name读取一次,使用驼峰式再读取一次。
        if ( getByName ) {
            ret = thisCache[ name ];
            if ( ret == null ) {
                ret = thisCache[ jQuery.camelCase( name ) ];
            }
        } else {
            //未传入参数name、data、则返回数据缓存对象。
            ret = thisCache;
        }
        return ret;
    },
    //用于移除通过jQuery.data()设置的数据
    removeData: function( elem, name, pvt /* Internal Use Only */ ) {
        ......
    },
    // 设置或读取内部数据时使用
    _data: function( elem, name, data ) {
        return jQuery.data( elem, name, data, true );
    },
    // 判断DOM元素是否可以设置数据,通过检查DOM元素的节点名称在对象jQuery.noData中是否存在
    acceptData: function( elem ) {
        var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ];
        return !noData || noData !== true && elem.getAttribute("classid") === noData;
    }
});

本节内容参考《jQuery技术内幕》第五章数据缓存Data

jQuery事件系统二

jQuery事件系统并没有将事件监听函数直接绑定到DOM元素上,而是基于数据缓存系统管理监听函数的。举一个简单的栗子看一下何为数据缓存系统:

<div class="box">
    <div class="content"></div>
</div>
<input type="button" name="submit" value="click me" class="btn" />

<script type="text/javascript">
    $(".box").click(function(){
        console.log("box");
    })
    $(".content").click(function(){
        console.log("content click 1");
    })
    $(".content").click(function(){
        console.log("content click 2");
    }) 
    $(".content").hover(function(){
        console.log("hover in");
    },function(){
        console.log("hover out");
    }) 
    $(".btn").click(function(){
        console.log("btn");
    }) 
</script>

如上述代码所示,页面上有一对父子元素box、content和一个按钮btn。

  • 父元素box绑定了click事件
  • 子元素content绑定了两个click事件,一个hover事件
  • btn元素绑定了一个click事件。

我们看一下chrome下面打印jQuery.cache是什么情况:
jQuery事件系统

由上图所示,

  • jQuery.cache中存储了三个以ID为key值的对象,三个ID是为页面中的box、content、btn元素分配的唯一ID。
  • 每个object中存储了events和handle两个对象。
  • events对象存储了click、mouseout、mouseover三个事件对象,click对象的值是一个存储了两个元素的数组,对应content元素的两个click事件和hover事件。
  • click[0]的数组元素中存储了一个guid,唯一标识事件处理函数的ID,以此类推jQuery.cache[1].events.click[0].guid == 1, jQuery.cache[2].events.click[1].guid == 3;存储了一个type=“click”;存储了一个事件处理函数handler,参见上图底部,是content元素绑定得第一个click事件的处理函数。
  • 那回到最外层,每个object的直接子元素handle是干什么用的?通过打印的内容可见,其方法体内调用了jQuery.events.dispatch事件。事实上,每个object对象都有一个handle作为入口监听函数,当浏览器触发事件时,入口监听函数被调用,该函数从事件缓存对象events中获取绑定到此元素上的对应的事件处理函数,然后执行。

问题来了,jQuery.cache中没有存储任何与页面元素有关的信息,其中的1、2、3是如何对应到DOM元素的?一定在什么地方给DOM元素设置了自定义属性存储了其ID值。

jQuery事件系统

如上图所示,代码来源于jQuery.event.add方法,给派发函数传递的第一个参数当前的DOM元素已经有一个自定义属性jQueryXXXX==2,接下来寻找何处设置了这个自定义属性。

jQuery.event = {

    add: function( elem, types, handler, data, selector ) {

        var elemData, eventHandle, events,
            t, tns, type, namespaces, handleObj,
            handleObjIn, handlers, special;

        //通过jQuery._data给当前DOM元素设置自定义数据
        if ( elem.nodeType === 3 || ... || !(elemData = jQuery._data( elem )) ) {
            return;
        }
        ......
    }
}

jQuery.extend({
    data: function( elem, name, data, pvt) {

        var thisCache, ret,
            internalKey = jQuery.expando, //这个自定义属性来自与expando,这是什么东西??
            getByName = typeof name === "string",

        if ( !id ) {
            // 给DOM元素设置一个unique ID,以获取全局的数据缓存对象中的对应数据
            if ( isNode ) {
                elem[ internalKey ] = id = jQuery.deletedIds.pop() || ++jQuery.uuid;
            } else {
                id = internalKey;
            }
        }
        ......
    },
    _data: function( elem, name, data ) {
        return jQuery.data( elem, name, data, true );
    },
},

好,我们知道了DOM元素的ID与jQuery.cache中的对象ID是一一对应的,在派发函数dispatch中通过种对应关系就能方便地找到当前DOM元素所绑定的事件从而执行。不过,顺着这个线索也遗留了一些问题,比如jQuery.expando是什么?jQuery.data除了DOM ID还设置了那些属性?事件派发函数中对于子元素的click事件冒泡到父元素是如何处理的?且听下回分解。

jQuery事件系统一

说到Javascript事件,脑海中闪现的是最初做简单网页时写个button,注册个onclick事件,就出现了简单的交互。那么onclick,和后续接触到的原生JS中的DOM0、DOM2、addEventListener、attachEvent、jQuery中的bind、live、delegate、on等是什么关系,如何演化的。一个简单的onclick为什么会发展成复杂的jQuery事件系统以及数据缓存系统。今天只是一个引子:

DOM0级事件系统

document.getElementById("btn").onclick = function(){
    alert(arguments.length);
}

这是是最简单的绑定事件的方式。再早之前就是元素标签内绑定事件,不推荐这种样式行为混在一起的写法。

优势:
简单稳定,浏览器兼容;处理事件时,this关键字处理的是当前元素,这很有帮助。
劣势:
1、只允许元素每次绑定一个回调,重复绑定会覆盖之前的绑定。
2、在IE下回调没有参数,在其他浏览器下回调的第一个参数是事件对象。上述代码在IE下弹出0,而在Firefox下弹出1,这个参数就是event对象。
3、只能在事件冒泡中运行,而非捕获或冒泡。

DOM2级事件系统

IE:

element.attachEvent("on" + type, callback); //绑定事件
element.detachEvent("on" + type, callback); //解除绑定
document.createEventObject(); //创建事件
element.fireEvent(type, event); //派发事件

优势:
可绑定多个事件,不会覆盖
劣势:
1、回调函数中this不是指向被绑定元素,而是window
2、同种事件绑定多个回调时,回调并不是按照绑定时的顺序依次触发(这还真是头一次听说)
3、event事件对象仅存在于window.event参数中,其属性与W3C的有很大差异,比如currentTarget
4、事件必须以onXXX形式,仅IE可用
5、只支持冒泡

W3C:

element.addEventListener(type, callback, [phase]); //绑定事件
element.removeEventListener(type, callback, [phase]); //解除绑定
element.createEvent(types); //创建事件
event.initEvent(); //初始化事件
element.dispatchEvent(event); //派发事件

优势:
1、同时支持事件处理的捕获和冒泡阶段,事件阶段取决于参数设置
2、事件处理函数内部,this引用当前元素
3、事件对象总是可以通过处理函数的第一个参数获取
4、可以绑定多个事件

劣势:其他标准浏览器的实现也有不一致的地方,比如firefox不支持focusin、focus事件,第三四五个参数的使用,事件成员对象的不稳定,如safari下event.target可能时返回文本节点。

为了兼容浏览器,我们通常会创建一个统一的方法,在方法内部通过特性检测分别调用不用的事件模型。

//绑定事件
function addEvent(el, type, callback, useCapture){
    if(el.addEventListener){//W3C方式优先
        el.addEventListener(type, callback, !!useCapture);
    }
    else{
        el.attachEvent("on" + type, callback);
    }
}
//移除事件
function removeEvent(el, type, callback, useCapture){
    if(el.removeEventListener){//W3C方式优先
        el.removeEventListener(type, callback, !!useCapture);
    }
    else{
        el.detachEvent("on" + type, callback);
    }
}
//派发事件
function fireEvent(el, type){
    if(el.createEvent){
        event = document.createEvent("HTMLEvents");
        event.initEvent(type, true, true);
        el.dispatchEvent(event);
    }
    else{
        event = document.createEventObject();
        el.fireEvent("on" + type, event);
    }
}
//延伸
//阻止冒泡的通用函数
function stopBubble(e){
    if(e && e.stopPropagation){ //W3C方式
        e.stopPropagation();
    }
    else{
        window.event.cancelBubble = true; //IE
    }
}
//阻止浏览器默认行为的通用函数
function stopDefault(){
    if(e && e.preventDefault){ //W3C方式
        e.preventDefault();
    }
    ele{
        window.event.returnValue = false; //IE
    }
}

fireEvent与调用onClick的区别:
派发事件fireEvent模拟用户行为触发事件,如触发一个button的onclick事件,如果该button未注册onclick事件也不会报错,并且会引发冒泡,触发其父类中的onclick事件,更贴近用户真实的触发行为。那如果直接调用onclick()呢?如果在未注册onclick事件时调用onclick将会报错“对象不支持此属性或方法”。

Dean Edward && event.js

鉴于DOM2级事件系统的缺陷,Dean Edward提出了更完美的解决方案,成为jQuery事件系统的源头,看它是不是长了三头六臂,代码来自http://dean.edwards.name/weblog/2005/10/add-event/

function addEvent(element, type, handler) {
    // 为每一个事件处理函数分派一个唯一的ID,方便移除
    if (!handler.$$guid) handler.$$guid = addEvent.guid++;
    // 为元素的事件类型创建一个空对象,保存所有类型的回调
    if (!element.events) element.events = {};
    // events对象包含多个type/handlers这样的键值对
    var handlers = element.events[type];
    if (!handlers) {
        handlers = element.events[type] = {};
        // 如果元素之前以onXXX的形式绑定过事件,则存储起来
        if (element["on" + type]) {
            handlers[0] = element["on" + type];
        }
    }
    // 保存当前的事件处理函数
    handlers[handler.$$guid] = handler;
    // 指定一个全局的事件处理函数来做所有的工作
    element["on" + type] = handleEvent;
};
// 事件处理函数ID计数器
addEvent.guid = 1;

function removeEvent(element, type, handler) {
    // 从events对象移除当前事件处理函数/函数类型
    if (element.events && element.events[type]) {
        delete element.events[type][handler.$$guid];
    }
};

//统一的事件处理函数入口
function handleEvent(event) {
    var returnValue = true;
    // 获取原生的事件对象
    event = event || fixEvent(window.event);
    // 从元素的事件对象上获取事件处理函数
    var handlers = this.events[event.type];
    // 遍历执行事件处理函数
    for (var i in handlers) {
        this.$$handleEvent = handlers[i];
        if(this.$$handleEvent(event)===false){
            returnValue = false;
        };
    }
};
//为IE的事件对象做简单的修复
function fixEvent(event) {
    //添加标准的W3C方法
    event.preventDefault = fixEvent.preventDefault;
    event.stopPropagation = fixEvent.stopPropagation;
    return event;
};
fixEvent.preventDefault = function() {
    this.returnValue = false;
};
fixEvent.stopPropagation = function() {
    this.cancelBubble = true;
};

特点:
1、没有对象检测,因为使用最通用的原始的onXXX绑定,不使用addEventListener/attachEvent
2、保持正确的this指向
3、传递了正确的event对象
4、完全跨浏览器包括IE4或NS4
5、不会引发内存泄漏(使用者发现onXXX在IE存在不可消弭的内存泄漏)

jQuery在这个版本基础上吸收了“每个处理函数分配一个unique ID,所有回调放到一个对象上存储”的建议,出现了jQuery的数据缓存系统,同时舍弃了onXXX方式,仍然使用addEventListener/attachEvent绑定事件。

<回到顶部>功能该谁做

当我用Macbook浏览长网页的时候,拉着拉着就得找<回到顶部>按钮。如果网页没有这个功能,就得自己拖回滚动条。为了更美观不碍眼,滚动条早已在不用的时候就隐藏起来,需要上下移动网页才显示,再将鼠标移动到小窄条上,左右手配合拖回顶部,真费劲。还不如在网页的鼠标右键上增加<回到顶部>的功能,单手操作两次点击即可。不如来个彻底,将<回到顶部>功能交给浏览器来做,顺便做个快捷键,也免得前端开发人员到处搞个<回到顶部>,或者什么时候闲来无事,搞个浏览器插件gotoTop,看看有没有人用。

Read More

香港之行

不想流水账一样写一篇攻略,如果有需要咨询的同学可以留言,必将知无不言,言无不尽。不想写”香港是自由与信念的国度“这样上纲上线的文章,只是就自己的所见发表一些所感。之前大Boss说我的视野有限,再不出去见识见识,就无法跟我交流了。我转了一圈回来了,恐怕也不能如他所愿感受到“外面的世界很精彩”。

车窗外的风景

自由:

说了不说还得说。自由是入关时让我摘下胸前背的双肩包检查是否怀孕。自由是进入地铁不再安检,也没有人在你身前身后探来探去。自由是地铁庞大的广告牌上写着“2017年要人手一票实现普选否则占领中环”。自由是路边书摊各种大陆政治内幕。不愧是当过英国殖民地,像是过给了有钱人家的孩子,现在回来了要求家长像有钱人家那样礼遇他。自由是社会进步的标识,我们被禁锢惯了,就皮实了,捏一下打一下也不还手,他们就很矫情,碰一下就叫。而在他们看来,我们又是什么人?没见过世面的乡巴佬,蹑手蹑脚地遵守规则。那又怎么样,自由迟早会有,你们只是走在前面而已。

寸土寸金:

香港真是一个寸土寸金的地方,我们住在红茶馆酒店,设施很完善,过道很狭小,同时只能容纳一个人通过。很多店铺也很小,打开大众点评app,选一个美食排名前三甲的餐厅好好挥霍一下,进去一看,小得还不如庆丰包子铺。

维多利亚港

美食:

吃了港式美食,我都汗颜觉得之前吃的怎么这么糙呢。就说那$86一碗的蟹黄粥,真的超级超级好吃。大米粥(号称白粥)里面放了青菜、虾仁、螃蟹和散落的蟹黄,尤其是蟹黄特别好吃。之前吃粥就是吃粥,顶多几种豆类的混合,绿豆粥、南瓜粥、大米粥、小米粥,谁家还往粥里放大螃蟹呀。搞个粥都这么复杂。说到复杂,我有一次随便吃了一个餐馆的“龙凤球”,面相更像是糖醋里脊,终于吃到一个内地菜了就很兴奋,吃着吃着内容就来了。挑起一块“里脊”,吃到里面是鸡肉,再挑一块,鱼肉,原来是这么个龙凤球。再说说配菜,洋葱、青辣椒、红辣椒、黄辣椒、菠萝块,我了个去,辣椒都配了三种,还放水果。真替厨师麻烦。

蟹黄粥

化妆品:

大街小巷到处都是卓悦、莎莎,这两家是化妆品零售店,包含国内各大专柜品牌的产品,但是价格低很多。她们就像从商场专柜无数闪光灯下走入一排排货架。一直没舍得买的“神仙水”就被放在货架底排一个角落。店里人多拥挤,听口音大多数是大陆人来扫货。像雪肌精、契尔氏这些大牌水水也就2、3百港币,感觉以前买的都被坑了似的,真是冤大头。街上的女孩皮肤都很好,如果我在这遍地都是化妆品店,用雅思兰黛跟用妮维雅一样普及的地方,我皮肤不好才怪呢。除了化妆品,还有很多奢侈品,手表、箱包、首饰,有一家店门口排了很长的队还有工作人员维持秩序,是为了给顾客更好的服务限制了进店人数,你猜大家排着队给人家送钱的店是啥?————香奈儿。脑子都进水了,刷卡都不要钱啊。

化妆品

排队:

这次经历了史上最长时间的排队,都想发誓再也不去人多的地方了。这悲惨的一天是在海洋公园度过的,早上出了地铁需要坐一趟巴士才能到海洋公园,沿着队走啊走啊都找不到队尾,快围了一大圈了,应该出门右转也许能近些。海洋公园里的热门项目均排队时间为2小时,这些都不说。天公不作美的是下午5点半准备出园的时候天降大雨。公园分为“高峰乐园”和“海滨乐园”两大主要景区,以登山缆车和海洋列车连接,上午我们坐缆车到“海滨乐园”,出园的时候需要回到”高峰乐园“,雨大缆车不能开放,只能排队坐海洋列车。我们在大雨里排了2个多小时,人挤人打伞也不管用,每个人的伞都挤得重叠,水顺着伞沿流到衣服里,只有内衣还算干的。7点半左右出园了,没想到搭巴士回去还要排很长的队,都快绝望了。

化妆品

这里面有一个细节,港人很多地方都主动排队,只有两三个人等巴士都会排队,这次队又长得看不到尾巴,就有很多插队的人,但是都被工作人员阻止,队里的人也会帮助维持秩序。有人插到我们的前面,工作人员劝说无效,我后面的哥们说“拉他出来,拉他出来,要去后面排队”。工作人员便拉他出来了,有些插队的人拉都拉不动,都在雨里淋了几个小时谁不想早点回家。然后在一个拐弯处我们这段队伍不知怎么搞得整个就插到前面去了,工作人员立即阻止但来不及了,只是阻止了很后面那一段。我后面那哥们不一样捡了个便宜嘛,怎么不见他说话了。

好吧,啰嗦至此,香港还是他们的,我们只有大陆。

海洋公园

迪士尼乐园

休假还是治疗?

在我正式休长假之前,有几周是双休的,为了缓和三年来的单休日子,让身体和心态从满负荷到倍轻松有一个缓冲。只是我有一个隐忧:周一正式开始工作的时候,总是需要1~2小时的“代入”,想一个很熟悉的CSS属性也慢了几秒钟。也许这是由休息进入工作状态避免不了的消耗。那休长假玩疯了之后如何开始工作?如何准备面试?认真地思考这个问题,是从我被电话面试问到我都面试过别人很多遍的必备技能时却语无伦次开始。我相信,我能回答的问题确实是我大脑深处印象最深的经历,不需要任何准备和“代入”便脱口而出,就像存储到大脑内存中一样。而我无法在任意时间内均可以解释清楚的问题是一遍又一遍备份到大脑硬盘中的,所以它不能像在内存中一样快速读取。

我想说的重点是两个:

  • 这样的电话面试不公平。面试者是有所准备的,被面试者是处于任意状态下的。不得不说一个很励志的场景,在一节在线托福口语班上,老师把话筒给了一个愿意在1000人的倾听下回答问题的学生,话筒切换之后听到一个小baby用着哭腔不停地呼唤着“妈妈”,这位“妈妈”学生边小声安抚着baby、边说了近一分钟的英语。就是说,你无法知道对方所处的状态,试想,一个人怎么能快速从满脑子想着小孩的状态切换到前端技术交流,当然你想着别的事情也是这样的。所以最好电话面试也要预约时间,或者被面试者直接婉拒,稍作准备后再接受。

  • 枕戈待旦。前端攻城师们真的要人如其名一样的枕戈待旦时刻准备攻城吗?我觉得最好是。我们在高中埋头苦海时家长说考上大学就轻松了,轻轻松松地上着大学然后就迎来了人山人海的就业大军,终于对工作可以轻松应对的时候发现囊中羞涩一平米也买不了。我们不再是相信考上大学就轻松的懵懂少年了,所以你最好也相信,休假不是只有玩,还要准备,时刻准备着。

最近迷上一部美剧《24小时》——放心,我没有在转移话题——每一季24集与现实时间同步地演绎24小时内发生的事情,每季间隔1~2年。主人公Jack Baver是一名反恐外勤人员,一名冷酷叛逆果断绝敢的冷血特工。每一季结束后他或者是被辞职过起寻常生活,或者是隐形埋名流浪天涯,或者是转去做文职,或者是被关进中国监狱,等到下一季他再出山,也完全是个身体素质极好枪法极准头脑极敏锐的特工。没有大腹便便没有身材发福没有技能退步,这真的是电视剧啊!

那么如何时刻准备着呢?为什么有些记忆近在内存,有些记忆远在硬盘呢?

借鉴一下我最近背单词的感悟,背单词有三种方法:

  • 重复记忆,这在我最开始学英语和日语时都得到了验证。初中英语老师要求每天写一篇16开纸的单词,写了三年。学日语一年之后为准备日语等级考试而背单词,每天按照记忆曲线重复背诵之前的内容,并开辟新的内容,大部分时间花费在重复以往上。小孩子学数数一定也是家长一遍又一遍地教才能数出来。这种方法试用于初学者,或者需要在有限时间内快速达到效果的时候。现在背托福词汇就是这样,每天花费1个小时快速浏览1200个单词。单词不是背出来的,是重复出来的。

  • 词根词缀法,比如pre这个词缀是预先的意思,那么你很容易就能背诵preview、previous。我很少用这种方法,因为大部分词汇都是重复出来的,有一定的词汇量之后才总结出各个词缀的意义,然后类推其他的时候也没有很管用。就像汉字的偏旁部首一样,你认识了“日”,又通过象形法“日”下画一条地平线就成了“旦”意味着早晨。但当你看到“但”这个字时还是不知道什么意思。

  • 联想记忆法,第一次学习/bus/这个单词时,你肯定在音标上写着/爸丝/这样的汉字,可能还有很多种/XX丝(死)/。将陌生的知识与已有经验经历建立对应关系,哪怕它们属于不同的知识体系,没有任何逻辑关系,只是方便记忆而已。

那么如何能够在长期的非工作期间保持对前端知识和工作经验的快速反应?

首先是重复,利用长假时间定期将以往的总结文档、收藏文章浏览一遍。其次保持思考,我们都加入过几个技术交流QQ群、也订阅过几个技术分享公众平台,诚实地说,我只是看一眼他们在说什么,并没有仔细思考他们提出的问题。大脑的懒惰就像是一个天然的屏障,将你与真知隔绝。而重复记忆法恰恰给大脑的懒惰找了一个冠冕堂皇的接口,就像是在说“不思考没关系,多看几遍就可以了”。从这个意义上来说,重复是个笨方法,思考才是捷径。文艺地说,时间可以治愈一切,任何事物都有遗忘的过程,重复是必备技能,而思考可以减缓遗忘,使重复的周期更长一些。

雅虎科技频道纯图片布局的实现(下)

雅虎科技

上一篇主要从外观上介绍了YahooTech的布局方式,本篇着重代码实现。代码实现经历了两个版本,以下是V1.0的算法:

1、 每张图片的宽度都设置为百分比,当屏幕resize时不需要额外处理。
2、 受启发于媒体查询根据屏幕宽度设置多种区间匹配样式,故设置0~320、320~1024、1024~1600、1600~+∞四个区间。

0~320:手机设备,每行只显示一个

320~1024:平板设备,每行显示二个或三个。第一个宽度为30~60%,第二个宽度为20~30%,或是当第一个宽度大于50%时,第二个占满该行。否则第三个占满该行。

1024~1600:桌面,每行显示三个或四个,类似上面的随机取值。

1600~+∞:超大宽屏,每行显示width/400张图片,每张图片宽度为350~450的随机值。

3、根据每张图片的实际宽高和显示宽度百分比计算其显示高度,并在当前行布局完成时取得当前行所有图片的最小显示高度作为当前行的显示高度。

insertItem : function(item)
{
    var divItem = $(item);
    var randomWidth = 0;
    //手机端每行只显示一个
    if(G_layout_options.clientW <= 320){
        randomWidth = 100;
        this.isFullROW = true;
        this.fullNum = 1;
    }
    //pad端每行显示二个或三个
    else if(G_layout_options.clientW <= 1024){
        if(this.itemsWidth.length == 0){
            randomWidth = Math.random() * (60 - 30) + 30;
        }
        else if(this.itemsWidth.length == 1){
            if(parseInt(this.itemsWidth[0]) < 50){
                randomWidth = Math.random() * (30 - 20) + 20;
            }
            else{
                randomWidth = 100 - this.itemsWidth[0];
                this.isFullROW = true;
                this.fullNum = 2;
            }
        }
        else if(this.itemsWidth.length == 2){
            randomWidth = 100 - this.itemsWidth[0] - this.itemsWidth[1];
            this.isFullROW = true;
            this.fullNum = 3;
        }
    }
    //desktop显示三个或四个
    else if(G_layout_options.clientW <= 1600){
        if(this.itemsWidth.length == 0){
            randomWidth = Math.random() * (40 - 20) + 20;
        }
        else if(this.itemsWidth.length == 1){
            if(parseInt(this.itemsWidth[0]) < 30){
                randomWidth = Math.random() * (30 - 20) + 20;
            }
            else{
                randomWidth = Math.random() * (40 - 30) + 30;
            }
        }
        else if(this.itemsWidth.length == 2){
            if(parseInt(this.itemsWidth[0] + this.itemsWidth[1]) < 60){
                randomWidth = Math.random() * (30 - 20) + 20;
            }
            else{
                randomWidth = 100 - this.itemsWidth[0] - this.itemsWidth[1];
                this.isFullROW = true;
                this.fullNum = 3;
            }
        }
        else if(this.itemsWidth.length == 3){
            randomWidth = 100 - this.itemsWidth[0] - this.itemsWidth[1] - this.itemsWidth[2];
            this.isFullROW = true;
            this.fullNum = 4;
        }
    }
    //超大屏
    else{
        this.fullNum = parseInt(G_layout_options.contentW / 400);
        if(this.itemsWidth.length < this.fullNum - 1){
            randomWidth = (Math.random() * (450 - 350) + 350) / G_layout_options.contentW * 100;
        }
        else{
            var itemsWidthSumTmp = 0;
            for(var i = 0; i < this.itemsWidth.length; i++){
                itemsWidthSumTmp += this.itemsWidth[i];
            }
            randomWidth = 100 - itemsWidthSumTmp;
            this.isFullROW = true;
        }
    }
    this.itemsWidth.push(randomWidth);
    divItem.css("width", randomWidth + "%");
    var that = this;
    divItem.appendTo(this.element);
    this.calcuRowHeight(divItem, randomWidth);
},

calcuRowHeight: function(divItem, randomWidth){
        var renderHeight = parseFloat(divItem.find("img").attr("data-height")) * (randomWidth * (G_layout_options.contentW - 20) - 1000) / (parseFloat(divItem.find("img").attr("data-width")) * 100);
        this.itemsHeight.push(renderHeight);
        if(this.isFullROW && this.itemsHeight.length == this.fullNum){
            //console.log(this.itemsHeight);
            var minHeight = Math.min.apply(Math,this.itemsHeight);
            this.element.css({"height":minHeight > 500? 500:minHeight});
            //每行显示两个时重新取bigger类型图片
            if(this.fullNum == 2){
                this.element.find(".content-img").each(function(){
                    $(this).attr("src",$(this).attr("src").replace("common","bigger"));
                })
            }

        }
    }

显示效果如下图:

立方媒体

其实这个算法很low,基本上就是一些随机值凑数,被吐槽都是卡卡西风格,这种不顾图片实际宽高而采用随机宽度的做法太不接地气,效果也不好。于是v2.0采用了新的解决方案:

1、 遍历待排列的数据块Blocks,取得一个availableRow,可能是新的一行,也可能是未满行,将当前数据块Block试插入此行。
2、 所谓试插入,就是计算新数据插入后的当前行宽度是否超出最大宽度,超出也没有关系,顺势按比例压缩计算试插入的行高。attempHeight = MaxWith/Sum(width/height)。此时设置一个高度边界值300,当行高小于300时影响效果,故试插入失败。其余情况皆为成功。
3、 试插入失败意味着当前行剩余空间过小,不适合再插入数据,故创建新行,新行变成了当前行。
4、 执行DOM插入操作。
5、 重新计算行高,并以此设置图片显示宽高,如果当前图片宽度与需要显示的宽度不符,为避免图片拉伸影响效果可重新获取对应尺寸的图片URL。排列完成后显示当前行。

在此方案中图片宽度使用固定值不再使用百分比,能够更大限度地因图制宜。只是窗口resize时需要重排。最大的亮点是行高的计算的方法:attempHeight = MaxWith/Sum(width/height),真的是很简单的四则运算提供了一个很大的突破口。

showBlocks : function(items)
{   
    for(var i = 0; i < items.length; i++)
    {
        var block = items[i];
        var row = this.getAvailableRow();
        var rowIndex = row.getRowIndex();
        if(!row.attemptInsertBlock(block)){
            row = this.createNewRow();
        };
        row.insertBlock(block);
    }
},

getAvailableRow : function()
    {
        var lastRow = this.getLastRow();
        if(lastRow == null)
            lastRow = this.createNewRow();
        else
        {
            var enough = lastRow.checkFullRow();
            if(enough){
                lastRow = this.createNewRow();
            }
        }
        return lastRow;
    },

attemptInsertBlock : function(block)
    {   
        //边界测试
        if(this.rowWidth + block.width - 10 > G_layout_options.contentW){
            this.rowWidth = G_layout_options.contentW;
            this.attempHeight = (G_layout_options.contentW - (this.blockWidths.length + 1) * 10 ) / (this.rowRadio + block.aspectRadio);
            if(this.attempHeight < 300){
                this.layoutBlock();
                return false;
            }
            return true;
        }
        return true;
    },

insertBlock : function(block)
    {
        //更新视图
        this.element.append(block.element);
        //更新Model
        this.blocks.push(block);
        this.attempHeight = this.attempHeight == 0? block.height : this.attempHeight;
        this.rowWidth += block.width + 10;
        this.rowRadio += block.aspectRadio;
        this.blockWidths.push(block.width);
        this.blockHeights.push(block.height);
        //边界测试
        if(this.rowWidth - 10 > G_layout_options.contentW){
            this.layoutBlock();
        }
    },

layoutBlock : function()
{   
    this.rowWidth = G_layout_options.contentW;
    this.attempHeight = (G_layout_options.contentW - this.blockWidths.length * 10 ) / this.rowRadio;
    //设置行高
    this.element.css({"height":this.attempHeight});
    for(var i = 0; i < this.blocks.length; i++){
        //更新Model
        var _block = this.blocks[i];
        _block.setContainer(this);
        _block.height = this.attempHeight;
        _block.width = _block.aspectRadio * this.attempHeight;
        if(_block.width > 400){
            _block.fetchImage = true;
            _block.src= _block.src.replace("common","bigger");
        }
        //更新视图
        _block.element.find("img").attr("width",_block.width);
        _block.element.find("img").attr("height",_block.height);
        if(_block.fetchImage){
            _block.element.find("img").attr("src",_block.src);
        }
    }
    this.isFullROW = true;
    //显示当前行
    this.show();
},

效果图如下:

立方媒体

有没有觉得顿时高大上了很多,完整效果请移步:http://www.l99.com/media/sex