ruirui's blog

ruirui's 备忘录


  • Home

  • Archives

iframe 打印页面局部内容

Posted on 2018-12-24
function printPartial(dom) {
      if (!dom) return;
      let copyDom = document.createElement('span');
      const styleDom = document.querySelectorAll('style, link, meta');
      Array.from(styleDom).forEach(item=> {
        copyDom.appendChild(item.cloneNode(true));
      });
      copyDom.appendChild(dom.cloneNode(true));
      const htmlTemp = copyDom.innerHTML;
      copyDom = null;

      const iframeDom = document.createElement('iframe');
      const attrObj = {
        height: 0,
        width: 0,
        border: 0,
        wmode: 'Opaque',
      };
      const styleObj = {
        position: 'absolute',
        top: '-999px',
        left: '-999px',
      };
      Object.entries(attrObj).forEach(([key, value])=> iframeDom.setAttribute(key, value));
      Object.entries(styleObj).forEach(([key, value])=> iframeDom.style[key] = value);
      document.body.insertBefore(iframeDom, document.body.children[0]);
      var iframedocument = iframeDom.contentDocument;
      var iframeWindow = iframeDom.contentWindow;
      iframedocument.open();
      iframedocument.write(`<!doctype html>`);
      iframedocument.write(htmlTemp);
      iframedocument.close();
      iframeWindow.onload = function() {
        iframeWindow.focus();
        iframeWindow.print();
        document.body.removeChild(iframeDom);
      }

注意:一定要在 iframe 里 windows 对象内容加载成功之后再去打印,iframe 的 print 方法是个阻塞的方法,类似 alert,会阻塞页面后续加载。

实现原生 bind 函数

Posted on 2018-12-20

bind() 方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind() 方法的第一个参数作为 this,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。

看 MDN 上关于bind的用法如下:

  1. 创建绑定函数
    bind() 最简单的用法是创建一个函数,不管怎么调用,函数都有同样的 this 值。
  2. 偏函数
    bind() 的另一个最简单的用法是使一个函数拥有预设的初始参数。只要将这些参数(如果有的话)作为bind()的参数写在 this 后面。
    例如:
    1
    2
    3
    4
    5
    6
    7
    function addArguments(arg1, arg2) {
    return arg1 + arg2
    }
    var result1 = addArguments(1, 2);
    // 创建一个函数,它拥有预设的第一个参数
    var addThirtySeven = addArguments.bind(null, 37);
    var result2 = addThirtySeven(5); // 37 + 5 = 42

上述37就是预设参数,函数 addThirtySeven 被调用时传入的参数会和之前的预设参数(37)一起传入 addThirtySeven 中。

  1. 作为构造函数使用的绑定函数。
    返回的函数可以作为构造函数使用(也就是可以用 new 创建)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function Point(x, y) {
    this.x = x;
    this.y = y;
    }

    Point.prototype.toString = function() {
    return this.x + ',' + this.y;
    };

    var emptyObj = {};
    var YAxisPoint = Point.bind(emptyObj, 0/*x*/);

    // 可以用 new 创建
    var axisPoint = new YAxisPoint(5);
    axisPoint.toString(); // '0, 5'

    axisPoint instanceof Point; // true
    axisPoint instanceof YAxisPoint; // true

    // 仍然能作为一个普通函数来调用
    YAxisPoint(13);
    emptyObj.x + ',' + emptyObj.y; // '0,13'

所以要是实现一个bind函数需要实现以下几个点:

  1. 创建一个新函数,替换 this 并返回新函数。
  2. 可以设置预设参数并不会丢失预设参数。
  3. 可以当做构造函数调用且有正确的this指向。

首先用猴子补丁

1
Function.prototype.bind = Function.prototype.bind || bind;

实现第一点:创建一个函数并返回新函数,将传入的第一个参数作为函数的 this 指向,使用 apply 或 call 来实现。

1
2
3
4
5
6
function bind (context) {
var self = this;
return function() {
return self.apply(context, arguments)
}
}

举例:

1
2
3
4
5
6
7
8
9
function fn() {
console.log(this); // 指向 obj
return Array.prototype.slice.call(arguments);
}
var obj = {
a: 1
}
fn.bind(obj, 1, 2)(); // []
fn.bind(obj, 1, 2)(3); // [3]

实现了绑定指定的 this 并返回新函数,也看到传入别的参数会丢失。接着实现第二点:可以设置预设参数,并且将预设参数以及之后调用的参数按顺序传入原函数。

1
2
3
4
5
6
7
8
9
function bind (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1); // arguments是类数组,把类数组对象转化成数组,因为apply里传的参数是数组。因为第一个arguments第一个参数是context,所以需要截取后面的参数。
return function() {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs); // 将 bind 传入的参数和调用返回函数传入的参数一起传入原函数中
return self.apply(context, finalArgs)
}
}

举例:

1
2
3
4
5
6
7
function fn() {
return Array.prototype.slice.call(arguments) ;
}
// args是[1,2] innerArgs是[]
fn.bind(null, 1, 2)(); // [1,2]
// args是[1,2] innerArgs是[3]
fn.bind(null, 1, 2)(3); // [1,2,3]

第三点:bind 绑定的函数可以当做构造函数来使用。
那现在如果用 new 创建的话会是什么情况呢?
举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Point(x, y) {
this.x = x;
this.y = y;
}
var emptyObj = {};
var YAxisPoint = Point.bind(emptyObj, 0);

var axisPoint = new YAxisPoint(5);
axisPoint.x // undefined
axisPoint.y // undefined

emptyObj.x // 0
emptyObj.x // 5

axisPoint instanceof Point; // false
axisPoint instanceof YAxisPoint; // true axisPoint是YAxisPoint的实例,但不是Point的实例

可以看到,用 new 创建对象实例时,并没有将构造函数的 this 指向实例化对象,bind 绑定的还是 emptyObj,目前和普通函数调用没有区别。
那 new YAxisPoint()时又做了什么呢?var a = new A(),new 一个对象主要做了这些操作:

  1. 创建一个对象。
  2. 将对象的 [[prototype]] 属性指向构造函数的原型对象上(一定是构造函数的原型对象)。
  3. 把新对象绑定到构造函数的 this。
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

所以,如果用 new 调用的时候,执行第二步会将新对象的 [[prototype]] 属性指向构造函数(YAxisPoint)的原型对象,所以axisPoint instanceof YAxisPoint是 true,而构造函数 YAxisPoint() 是

1
2
3
4
5
return function() {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return self.apply(context, finalArgs)
}

context 是调用 bind 时传进去的 empty,调用 Point 函数的还是 empty,也就是说上下文指向的是传入的empty,并没有把新对象 axisPoint 绑定上,所以会发现

1
2
3
4
emptyObj.x // 0
emptyObj.x // 5
axisPoint.x // undefined
axisPoint.y // undefined

所以我们现在要做的是,如果是 new 创建的对象,需要把调用函数的 this 指向新对象(也就是构造函数 this,看下面分析),那怎么判断是 new 调用的构造函数呢?我们来打印一下调用时 this 的指向。加上 new 调用的判断:

1
2
3
4
5
6
7
8
9
10
11
function bind (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var resultFunc = function() {
console.log(this); // 打印this
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return self.apply(context, finalArgs)
}
return resultFunc;
}

发现如果是普通函数调用,this 指向的是全局的 Window 对象 Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …},如果是 new 调用,this 指向的是 resultFunc 这个函数resultFunc {}。
再反过来看 new 一个对象的过程第三步:把新对象绑定到构造函数的 this,也正好说明了这点:new调用时,this指向的是构造函数的this。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function bind (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var resultFunc = function() {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
if (this instanceof resultFunc) {
return self.apply(this, finalArgs)
} else {
return self.apply(context, finalArgs)
}
}
return resultFunc;
}

看到axisPoint可以正常赋值,但是axisPoint instanceof Point为 false

1
2
3
axisPoint.x // 0
axisPoint.y // 5
axisPoint instanceof Point // false

还需要将构造函数的原型指向原本调用函数 self 的原型,所以需要加上resultFunc.prototype = self.prototype。而一般实现这种继承都需要加入一个中间函数,否则改变 resultFunc.prototype 会改变 self.prototype 的值。所以最终代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function bind (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var resultFunc = function() {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
if (this instanceof resultFunc) {
return self.apply(this, finalArgs)
} else {
return self.apply(context, finalArgs)
}
}
var F = function () {};
F.prototype = self.prototype;
resultFunc.prototype = new F();
return resultFunc;
}

flex-grow 子元素高度百分比无效

Posted on 2018-12-07

chrome 浏览器,flex 布局,设置 flex-grow 部分的子元素无法用百分比适应父元素尺寸,而 flex:1 可以。例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!DOCTYPE html>
<html>
<style>
#wrapper {
height: 800px;
display: flex;
flex-direction: column;
}
header, footer {
height: 30px;
}
main {
/* flex: 1; */
flex-grow: 1;
background-color: red;
}
#test {
height: 100%;
background-color: yellow;
}
</style>
<body>
<div id="wrapper">
<header>
header
</header>
<main>
<div id="test">
我是id为test的div,我设置了height: 100%
</div>
</main>
<footer>
footer
</footer>
</div>
</body>
</html>

当 main 设置为 flex-grow 时,子元素设置 100% 无效。不能撑大到父元素的高度。

为什么呢:
看W3C上关于高度的定义:

10.5 Content height: the ‘height’ property
percentage
Specifies a percentage height. The percentage is calculated with respect to the height of the generated box’s containing block. If the height of the containing block is not specified explicitly and this element is not absolutely positioned, the value computes to auto.
auto
The height depends on the values of other properties.

即:如果高度设置百分比,那百分比是计算的相对于包含块的高度。如果包含块高度未指定(取决于内容高度),而且此元素不是绝对定位,那该百分比值就等同于auto。

此外,chrome 也接受:当父元素用了 flex-grow ,同时也有 flex-basis 属性(固定的任何值,包括flex-basis:0)时,子元素可以引用父元素的高度。

即此例子是可以的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html>
<style>
#outer {
display: flex;
flex-direction: column;
height: 300px;
background-color: white;
border: 1px solid red;
}
#middle {
flex-grow: 1;
flex-basis: 1px;
background-color: yellow;
}
#inner {
height: 100%;
background-color: lightgreen;
}
</style>
<body>
<div id="outer">
<div id="middle">
<div id="inner">
INNER
</div>
</div>
</div>
</body>
</html>

那为什么 flex:1 可以呢?
flex 是 flex-grow,flex-shrink,flex-basis 的缩写。
flex(default):

1
2
3
flex-grow: 0
flex-shrink: 1
flex-basis: auto

flex:1:

1
2
3
flex-grow: 1
flex-shrink: 1
flex-basis: 0

flex-grow:1:

1
2
3
flex-grow: 1;
flex-shrink: 1;
flex-basis: auto;

所以二者主要区别在于flex-basis。

flex-basis: auto:main 元素的高度由内容高度来决定,flex-grow 分配剩余空间(800px-60px),但是是一个伪分配,flex-grow 更像是一个min-height。
flex-basis: 0:等同于 main 元素的高度是0,flex-grow 将剩余空间实实在在的分配给 main 元素,给 main 元素增加高度。

解决办法是:

  1. 给父元素指定一个固定高度, min-height 或 max-height 不行。
  2. 将父元素 main 设置为 relative,子元素设置 absolute 使其脱离文档流定位。
  3. 嵌套 flex 布局(推荐用法)。设置 main 为display:flex作为 flex 容器,align-items 默认值为 stretch ,这样子元素会自动拉伸以适应父容器的高度。但要注意:移除内部元素的高度属性(height:100%),否则会忽略父元素的align-items:stretch 。为了使 stretch 属性起作用,内部高度必须设置为 auto 。

参考链接
http://zhoon.github.io/css3/2014/08/23/flex.html
https://stackoverflow.com/questions/33636796/chrome-safari-not-filling-100-height-of-flex-parent

由 transform 引发的 bug

Posted on 2018-12-07

在 vue 项目的某页面用 better-scroll 滚动条组件的过程中发现:刷新当前页面滚动条显示正常,从其他页面切换过来显示异常(有滚动条但显示异常)。

第一反应是被别的页面样式影响到了,排查没问题。

接着分析 better-scroll 可能出现滚动条异常的原因:1.层级关系出错,2.计算高度出错。

  1. 层级关系

外层 wrapper 层,固定高度,且设置overflow:hidden。内层第一个子元素content层,高度大于 wrapper 显示滚动条。页面出现滚动条,不是此问题。

  1. 计算高度出错

打印BS对象,查看hasVerticalScroll、scrollerHeight和wrapperHeight值。如果是因为未出现滚动条,则一般scrollerHeight小于wrapperHeight。可能是因为在数据未渲染成功时加载滚动条导致。

查看内置属性,发现外层高度wrapperHeight在切换页面时会出现塌陷。获取数据后用 setTimeout 延迟几秒就正常,以为是常见的数据未渲染成功时就加载导致的,分析这个过程又持续了好久无果。又发现设置 setTimeout 延迟 3 秒就正常,延迟时间小于 1 秒就失败,限制网速也可以正常显示。这就很诡异了,又不是数据渲染的问题,但延迟时间又有效果。折腾好久还是没找到问题。
(其实当时重点关注不应该是这里,应该主要分析切换页面高度异常这个问题)

又一层层分析打印各个元素及其高度,发现有元素打印出来指向不到页面选中的状态,也就是说,虽然控制台打印出来了,但是元素不在这个页面,这个问题很重要了,为什么会出现这种情况呢,难道有动画?往上层找发现页面的外层框架(d2-admin)在某个页面(路由)切换的时候会有一个0.5秒的动画,同时也发现此页面最外层 div 高度是 0 (flex-grow 子元素设置百分比无效)。外层框架是 flex布局。而且只有在有 transform 属性的时候才会出现这个问题。

用一个简单的例子还原一下。
demo

延长动画的时间,并将 page 设置成固定高度,比较明显的看到在动画结束前内层元素 page-inner 都没有撑开到正常的高度。当去掉动画或者将外层 page 设置为position:absolute,都可以解决问题。

那么问题来了,transform 动画为什么会导致内层元素高度失效?设置为绝对定位为什么就不受动画的影响?

transform会提升元素的垂直地位,使元素表现出position:relative的特性,导致堆叠上下文(Stacking Context)和包含块(Containing Block)的创建,其后代元素(含有fixed属性或absolute属性)会以该父元素作为包含块。而当父元素page元素为position:relative的时候,子元素 page-inner 会根据父元素来定位,所以导致高度为 0 ,去掉动画后,也就没有了transform属性。就根据再上一级来定位。

至此,解决了花费时久的坑。

JS 事件运行机制

Posted on 2018-09-25

网上看了好多关于 JS 事件循环的文章,从各个文章中吸收了点自己需要的,整理总结如下。

JS 运行机制

JS是单线程的,某一时刻只能执行特定的一个任务,并且会阻塞其他任务执行,浏览器使用事件循环(Event Loop)机制来处理事件。

事件循环的任务队列有两种:macro task 和 micro task。
常见的 macro task 有:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件。
常见的 micro task 有:promise.then、process.nextTick、MutationObserver。

一次完整的 Event loop 过程如下:

  • 初始状态:调用栈为空,micro task 为空,macro task 有script(整体代码)。
  • 接着:执行macro task 中的 script 代码,碰到同步任务,推入调用栈执行,碰到异步任务,将其分发到对应的任务队列中。此时,macro task 中的第一个任务(script)执行完了,script 被移出 macro 队列,
  • 然后:开始清空 micro task 队列。也就是,执行一个 macro 队列,清空所有的 micro 队列。
  • 执行渲染操作,更新界面。
  • 检查看是否有 web worker 任务,有的话则处理。

每一次循环都是这样一个过程:
event-loop
以一个简单例子描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
console.log('script start')

setTimeout(function() {
console.log('setTimeout')
}, 0)

new Promise(resolve => {
console.log('Promise')
resolve()
}).then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})

console.log('script end')

分析一下执行过程:

  • 初始状态:macro 中是整体的 script 代码。
  • 开始执行 script,第一个console.log(‘script start’),直接输出。
  • 接着碰到 setTimeout,将其放入 macro 中。
  • 接着 new promise 直接打印出 Promise,因为 Promise 实例一旦创建,执行器立刻执行,后续的.then放入 micro 中。
  • 直接打印 script end。
  • 此时,macro 中的第一个任务结束,要开始清空 micro 了,于是输出、promise1、promise2。
  • 接着执行下一轮事件循环,执行下一个 macro task,输出 setTimeout。

于是,最终输出顺序是:script start => Promise => script end => promise1 => promise2 => setTimeout。

如果加上 async/await 呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
console.log('script start')

async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()

setTimeout(function() {
console.log('setTimeout')
}, 0)

new Promise(resolve => {
console.log('Promise')
resolve()
}).then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})

console.log('script end')

加上 async/await 发现「async1 end」的输出顺序有了差异。
chrome 70 版本是:

1
script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout。

chrome 73 版本是:

1
script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout。

所以问题是await 做了什么?async/await 其实是 promise 的语法糖,promise 的事件循环机制理解了,那将 await 转化为 promise 就好理解了。

我们知道 async 函数总会返回一个 promise,看官方规范:
也就是说:

1
2
3
4
async function async1() {
await async2()
console.log('async1 end')
}

等价于:

1
2
3
4
5
function async1() {
return RESOLVE(async2).then(()=>{
console.log('async1 end')
})
}

而 RESOLVE() 几乎等于 Promise.resolve()。几乎等于的意思就是还是有区别,下面看具体区别。

RESOLVE() vs Promise.resolve()

如果参数是个非 thenable 值,那么 Promise.resolve(non-thenable) 等价于 RESOLVE(non-thenable)。
但如果是个 thenable 对象,就不相等了,因为

1
2
3
4
// RESOLVE()
new Promise((resolve, reject) => {
resolve(thenable)
})

等价于

1
2
3
4
5
new Promise((resolve, reject) => {
Promise.resolve().then(() => {
thenable.then(resolve)
})
})

也就是说:RESOLVE() 会产生一个新的 promise,尽管该 promise 会 resolve,但这个过程是异步的,所以进入队列的是 Promise.resolve 的 then 过程,thenable 的 then 要等到执行到该 promise 的 then 之后才能执行,所以时序会靠后,举个明显的例子:

1
2
3
4
5
6
7
8
9
10
11
12
// RESOLVE(thenable)
let p1 = Promise.resolve(1)
new Promise((resolve, reject) => {
resolve(p1)
}).then(res => {
console.log(res)
})
p1.then(res => {
console.log(2)
})
//2
//1

但

1
2
3
4
5
6
7
8
9
10
// Promise.resolve(thenable)
let p1 = Promise.resolve(1)
Promise.resolve(p1).then(res => {
console.log(res)
})
p1.then(res => {
console.log(2)
})
//1
//2

回到上面await:

1
2
3
4
async function async1() {
await async2()
console.log('async1 end')
}

等价于:

1
2
3
4
5
function async1() {
return RESOLVE(async2).then(() => {
console.log('async1 end')
})
}

再等价于:

1
2
3
4
5
6
7
8
9
function async1() {
new Promise((resolve, reject) => {
Promise.resolve().then(() => {
async2().then(resolve)
})
}).then(() => {
console.log('async1 end')
})
}

所以,「async1 end」的输出会靠后。

Await 规范的更新

根据最新的TC39决议,await 将直接使用 Promise.resolve() 相同语义。

也就是说:

1
2
3
4
async function async1() {
await async2()
console.log('async1 end')
}

等价于:

1
2
3
4
5
function async1() {
Promise.resolve(async2).then(()=>{
console.log('async1 end')
})
}

Promise.resolve(p): 如果传入的 p 是一个 promise 实例,则会直接返回 p,不做任何修改,所以再次等价于:

1
2
3
4
5
function async1() {
async2().then(()=>{
console.log('async1 end')
})
}

chrome canary 73 采用了这种实现,所以输出会靠前。

参考文章:
https://github.com/xianshenglu/blog/issues/60
https://segmentfault.com/q/1010000016147496

从一道题理解观察者模式(自定义事件)

Posted on 2018-08-28

看到过许多关于观察者模式的介绍,大都是大篇理论性的概念开始,看的云里雾里的,直到看到这道题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
请实现下面的自定义事件 Event 对象的接口,功能见注释(测试1)
该 Event 对象的接口需要能被其他对象拓展复用(测试2)

// 测试1
Event.on('test', function (result) {
console.log(result);
});
Event.on('test', function () {
console.log('test');
});
Event.emit('test', 'hello world'); // 输出 'hello world' 和 'test'

// 测试2
var person1 = {};
var person2 = {};
Object.assign(person1, Event);
Object.assign(person2, Event);
person1.on('call1', function () {
console.log('person1');
});
person2.on('call2', function () {
console.log('person2');
});
person1.emit('call1'); // 输出 'person1'
person1.emit('call2'); // 没有输出
person2.emit('call1'); // 没有输出
person2.emit('call2'); // 输出 'person2'
var Event = {
// 通过on接口监听事件eventName
// 如果事件eventName被触发,则执行callback回调函数
on: function (eventName, callback) {
//你的代码
},
// 触发事件 eventName
emit: function (eventName) {
//你的代码
}
};

首先看测试1,需要实现一个 Event 对象,用 on 方法注册了两个 test 属性的事件,当 emit 触发事件时,触发 test 属性对应的回调。on 方法也就是注册事件,需要传两个参数,第一个是事件类型,第二个是该事件的回调,当调用 emit 方法的时候,触发对应属性的回调。总结来说,主要就是实现两点:注册事件和触发事件。这就是典型的观察者模式啊!
通过上面分析,要写的代码就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var Event = {
subs: {},
on: function(eventName, callback) {
if(!this.subs) {
this.subs = {};
}
if(!this.subs[eventName]) {
this.subs[eventName] = [];
}
this.subs[eventName].push(callback);
},
emit: function(eventName) {
var arg = arguments[1];
if(!eventName) {
return
}
this.subs[eventName] && this.subs[eventName].forEach(function(item){
item(arg);
})

}
}

以上面的例子简单理解一下观察者模式:

观察者模式

观察者模式也叫发布-订阅模式(当然这两个有区别,后续说区别),定义了对象的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都得到通知。由 Subject(主体)和 Observe(观察者)组成,Subject 负责发布事件,Observe 通过订阅事件来观察 Subject,一旦 Subject 发布更新,Observe 会得到通知。DOM 事件就是典型的观察者模式,元素注册事件,当触发事件时调用注册的回调。

1
2
3
el.addEventListener('click',function(e){
console.log(e);
}, false)

观察者模式 VS 发布-订阅者模式

观察者模式

观察者模式中,观察者直接订阅目标事件,目标改变时,调用观察者的通知方法,是一种紧耦合的状态。

observe.jpg

实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Subject {
constructor() {
this.subs = [];
}
subscribe(sub) {
this.subs.push(sub);
}
update() {
this.subs.forEach(sub=>{
sub.notify();
})
}
}
class Observer {
constructor(data) {
this.data = data;
}
notify() {
console.log(this.data)
}
}

let subject = new Subject();
let obj1 = new Observer('hello');
let obj2 = new Observer('world');
subject.subscribe(obj1);
subject.subscribe(obj2); // obj1 和 obj2 订阅了 subject
subject.update(); // subject 更新通知所有的 观察者 obj1 和 ob2,调用 notify 方法

发布-订阅模式

发布-订阅模式是广义的观察者模式,发布-订阅模式会抽离出一个调度中心(Event Bus),负责对订阅者的管理,使得发布者和订阅者解耦,便于扩展。

pub-sub.jpg

Vue 非父子组件通信方式 Event Bus 就是这种模式。通过 Event Bus 管理事件。

1
2
3
4
5
6
7
8
// 全局注册事件中心
var bus = new Vue();

// 在组件 A 中注册监听事件
bus.$on('id-selected', function(){});

// 在组件 B 触发事件
bus.$emit('id-selected','');

实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class EventBus {
constructor() {
this.subs = {};
}
on(subject, callback) {
if(!this.subs[subject]) {
this.subs[subject] = [];
}
this.subs[subject].push(callback);
}
emit(subject, params) {
if(!this.subs[subject]) {
return
}
this.subs[subject].forEach(callback=>{
callback(...params);
})

}
}

const event = new EventBus();
event.on('update', function(data){
console.log(data);
})
event.emit('update', 123);

接着回到题目看测试2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 测试2
var person1 = {};
var person2 = {};
Object.assign(person1, Event);
Object.assign(person2, Event);
person1.on('call1', function () {
console.log('person1');
});
person2.on('call2', function () {
console.log('person2');
});
person1.emit('call1'); // 输出 'person1'
person1.emit('call2'); // 没有输出
person2.emit('call1'); // 没有输出
person2.emit('call2'); // 输出 'person2'

为两个 person 注册独立的事件,如果按之前的代码,测试输出:

1
2
3
4
person1
person2
person1
person2

也就是,两个 person 并没有互相独立,person1 注册的方法 person2 也会有。因为有个 Object.assign() 方法,Object.assign(target, source)用于将源对象(source)的所有可枚举的属性,复制到目标对象(target)中,这个方法只是第一层属性的拷贝,如果属性的值是个对象,那值就是对象的引用,所以两个 person 里的 subs 是同一个引用。
解决方法最简单的是实现深度克隆,但题目已经固定了方式,所以必须将 subs 设置为不可枚举的属性。这样在每个 person 调用的时候都产生新的 subs。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var Event = {
on: function (eventName, callback) {
if(!this.subs) {
Object.defineProperty(this, "subs", {
value: {},
enumerable: false,
configurable: true,
writable: true
})
}

if(!this.subs[eventName]){
this.subs[eventName]=[];
}
this.subs[eventName].push(callback);
},
emit: function(eventName) {
var arg = arguments[1];
if(!eventName) {
return
}
this.subs[eventName] && this.subs[eventName].forEach(function(item){
item(arg);
})
}
};

『译』Webpack Import 异步加载

Posted on 2018-05-08

原文链接:Webpack and Dynamic Imports: Doing it Right

webpack和异步加载

在webpackv1中,我们推荐使用AMD的require或者webpack的require.ensure来实现动态加载模块。但在这篇文章,我们将介绍webpackV2支持的ES2015 dynamic import方式来加载模块,需要用到babel插件和webpack一些特性。

基础用法

语法比较简单:

1
import("module/foo").then(foo => console.log(foo.default))

上面的代码将在运行时加载foo模块,打印出模块的默认输出。import接收一个字符串参数。

动态加载模块的语法

假设你的应用在移动端和PC端有不用的展现方式,只有一种响应式的设计不能满足要求的,那么就需要在不同的设备上加载不同的页面去渲染。作为一个聪明的开发者,如果用户使用的是移动端那么你是不希望去加载PC端的代码,反之亦然。那么将会用以下方式处理:

1
2
3
4
5
6
7
8
9
export default function pageLoader(platform) {
switch (platform="desktop") {
case "mobile":
return import("components/MyMobileComponent");

case "desktop":
return import("components/MyDesktopComponent");
}
}

“但是,这只是一个简单的例子,真正项目里不会只有一个页面要处理”

所以,尽管我们使用了组件的动态加载方式,但还是不够灵活,让我们重构一下代码:

1
2
3
4
5
6
7
8
export default function pageLoader(platform, componentName) {
switch (platform="desktop") {
case "mobile":
return import(`components/mobile/${componentName}`);
case "desktop":
return import(`components/desktop/${componentName}`);
}
}

“还是不太好,应用不只是支持这两个设备,以后可能会支持更多的设备”

1
2
3
4
5
const load = (platform="desktop") => componentName => import(`components/${platform}/${componentName}`);
export const loadDesktopComponent = load("desktop");
export const loadMobileComponent = load("mobile");

export default load;

在这个例子中,我们采用了更优雅的方法。可以适用任何我们想用的平台。

加载文件存在的问题

在webpack中通常我们处理图片的方式是使用file-loader,file-loader会将文件路径映射到模块内。

1
import(`assets/images/${imageName}.jpg`).then( src => ... )

现在存在的问题是:如果你想要动态加载一个文件,像这个例子中的图片,webpack会生成一个额外的chunk:

1
2
3
4
5
6
7
8
9
10
11
webpackJsonp([4],{

/***/ 850:
/***/ (function(module, exports, __webpack_require__) {

module.exports = __webpack_require__.p + "6089b36e59a28c41600c17626366cde0.jpg";

/***/ })

});
//# sourceMappingURL=4.chunk.a6e0a95123529f9afcc5.js.map

所以最大的问题是: 当你请求一个异步加载的图片时,会先发一个网络请求去请求该模块的chunk,再去请求图片。每个图片都需要请求两次才能加载成功,如果使用https的话那么就更糟糕了。

那么如何解决这个问题呢:

webpack“神奇的注释”

webpack为动态引入增加了一个非常好的特性:神奇的注释。在配置中添加一些注释,以告诉webpack如何创建并加载该chunk。

Webpack Mode

1
import(/* webpackMode: "eager" */ `assets/images/${imageName}.jpg`)

这将使webpack把该异步加载的chunk加入到其父chunk中,不再去单独创建一个chunk。这种方式下,所有的文件路径在父chunk加载的时候就加载进来了。
webpack有四种模式(lazy,lazy-once,eager,weak)来处理异步加载。具体看这里。

Webpack Chunk名称

“hey,我注意到webpack将异步加载的模块命名为数字,这样调试起来有点麻烦,因为我不知道某个特定的chunk是否加载!”

我们既然可以控制加载方式,那么也可以通过注释去改变chunk的名称,例如:

1
2
import(/* webpackChunkName: "foo-image" */ "assets/images/foo.jpg");
import(/* webpackChunkName: "bar-module" */ "modules/bar");

Prefetch/Preload

注:这个特性在webpackV4.6之后才有

如果你使用的是http2,那么最好将大chunk分割成小的chunk。所以,最好的方式是将异步加载的chunk和父chunk分开并且在请求图片前加载好图片的chunk。
那就需要用webpackPrefetch: true来替代webpackMode: eager。

“那么Prefetch和Preload有什么区别吗?”

webpack的文档解释的更详细:

  • A preloaded chunk starts loading in parallel to the parent chunk. A prefetched chunk starts after the parent chunk finish.
    preload:异步chunk和其父chunk是并行加载的。prefetch:异步chunk是在父chunk加载完之后再加载的。
  • A preloaded chunk has medium priority and instantly downloaded. A prefetched chunk is downloaded in browser idle time.
    preload:异步chunk在浏览器中有中等优先级,会立即下载。prefetch:异步chunk会在浏览器空闲的时候下载。
  • A preloaded chunk should be instantly requested by the parent chunk. A prefetched chunk can be used anytime in the future.
    preload:异步chunk在父chunk加载完之后会立刻发起请求。prefetch:异步chunk会在父chunk加载完之后的任意时间发起。
  • Browser support is different.

浏览器支持是有差异的

因为prefetch chunk会在浏览器空闲的时候加载,所以可以加数字注释说明chunk加载的顺序。

1
2
3
4
5
6
// 0 is same as true
import(/* webpackPrefetch: 0 */ "assets/images/foo.jpg");
// this loads first as 1 > 0 (or true)
import(/* webpackPrefetch: 1 */ "modules/bar");
// this one will be the last!
import(/* webpackPrefetch: -100 */ "modules/slowpoke");

顺序将是:bar>foo>slowpoke。

深入研究代码分割

如果你想要深入研究在单页应用中是如何进行懒加载的,可以看我之前的两篇文章,虽然是用react作为例子的,你可以在任何基于SPA的框架/库中运用相同的思想:

Lazy Loading with React and Webpack 2

Lazy Loading with React + Redux and Webpack 2

结束语

代码分割是一个强大的功能,可以使应用变得更快,更智能的去加载依赖关系。但是正如uncle Ben曾经说的:”with great power comes great responsibility” 。了解工具是如何工作的才能最大限度发挥他的性能,希望这篇文章能对你有所帮助!

Promise 踩坑记

Posted on 2018-01-22

在工作中遇到Promise的很多坑,一直想着对这些知识点进行总结。

javascript的异步处理,大多会想到通过回调函数来解决,但回调带来的问题也比较明显:

  1. 在多层嵌套的回调函数中,无法判断什么时候完成异步,需要在外层作用域声明一些变量,这样容易被其他函数或变量修改。
  2. 因为异步函数在新的栈中运行,无法获取到之前栈的信息,之前的栈也无法捕获新的栈中抛出的错误,无法用try-catch处理错误。

Promise可以将异步处理模块化,规范化。使得代码更简洁,优雅,可读性更好。

基础用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var promise = new Promise((resolve, reject)=>{
// ... some code

if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
})
promise.then((value) => {
// success
}.catch((err)=>{
// fail
})

Promise 构造函数内有个执行器,将要进行的异步操作放入执行器中,Promise 实例一旦创建,执行器立即执行,执行器执行完毕后改变实例的状态,接着去执行相应的函数。Promise 的状态只能由执行器改变。

Promise 实例的特点:

  1. 对象状态不受外部影响,只有内部执行器可以改变状态。
  2. 状态一旦改变,就不会再变,任何时候都可以得到这个结果。

Promise的链式调用

Promise支持链式调用,因为调用.then方法每次都会返回新的 Promise 对象。
状态响应函数(即 then 方法)的返回值可以是以下三种

  1. 一个 Promise 对象
  2. 一个同步的值或者是 undefined
  3. 同步的 throw 一个错误
  • 如果返回的是一个 Promise 对象(即异步操作),那后一个回调函数(.then()方法),会等待该 Promise 对象状态发送变化,才会去执行。
  • 如果返回一个同步的值,会默认“立刻”执行下一个.then(),如果不返回值,会传入一个 undefined,但不影响接下来的执行。同时将同步的值转化为 Promise 风格的代码。
  • 如果.then 方法里 throw new Error(), 会被 catch 捕获到。

Promise.resolve()

会返回一个 fulfilled 的 Promise 实例或原始的 Promise 实例。

Promise.resolve():当参数为空,返回一个状态为 fulfilled 的 Promise 实例。
Promise.resolve(object):当参数是 Promise 无关的值,同上,不过 fulfilled 响应函数会得到这个值。
Promise.resolve(promise):当参数是 Promise 实例,则返回该实例,不做任何修改。
Promise.resolve(thenable):当参数是.thenable,立刻执行 then 函数。

Promise.reject()

返回一个使用接收到的值进行了 reject 的新的 Promise 对象。
和 Promise.resolve() 的区别是:当参数为 Promise 对象时,返回一个全新的 Promise 对象。

Promise使用常见错误

注意一:.then 方法每次返回一个新的 Promise 对象。区分链式调用和非链式调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1: 对同一个 Promise 对象同时调用 `then` 方法
var aPromise = new Promise(function (resolve) {
resolve(100);
});
aPromise.then(function (value) {
return value * 2;
});
aPromise.then(function (value) {
return value * 2;
});
aPromise.then(function (value) {
console.log(value); // => 100
})
// 2: 对 `then` 进行 promise chain 方式进行调用
var bPromise = new Promise(function (resolve) {
resolve(100);
});
bPromise.then(function (value) {
return value * 2;
}).then(function (value) {
return value * 2;
}).then(function (value) {
console.log(value); // => 100 * 2 * 2
});

下面是一个由 then 方法导致的比较容易出错的例子

1
2
3
4
5
6
7
function anAsyncCall() {
var promise = doSomethingAsync();
promise.then(function() {
somethingComplicated();
})
return promise;
}

这种错误也是 Promise 的反模式,更多反模式参考这篇文章。

这样会存在很多问题: 首先在 somethingComplicated 方法中产生的异常不会被外部捕获,此外,也不能得到 then 的返回值。当最后返回的是第一个 Promise 而不是 Promise 调用 then 方法后的结果,Promise 链也随机断掉。

正确的做法应该是:

1
2
3
4
5
6
function anAsyncCall() {
var promise = doSomethingAsync();
return promise.then(function() {
somethingComplicated()
});
}
1
2
3
4
5
function anAsyncCall() {
return doSomethingAsync().then(function() {
somethingComplicated()
});
}

注意二:.then 方法执行完必须将结果 return 出来。

.then 方法支持链式调用,不 return 的话,then 里的方法就会返回 undefined。所以要养成在 then 方法内部永远显式的调用 return 或throw 的习惯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function test(){
return new Promise(function (resolve) {
resolve(100);
}).then(function (value) {
Promise.reject(value * 2);
// return Promise.reject(value * 2);
});
}
test().then(function (value) {
console.log('true')
console.log(value);
}, function(value){
console.log('false')
console.log(value);
});
// true undefined
// 如果.then将结果return出来,结果是
// false 200

注意三:catch 和 then 的区别

catch 是 then 的语法糖,它是then(null, rejection)的别名,也就是当 Promise 对象状态变成 rejected 时会执行 catch,但是catch 调用完之后还是会返回一个 Promise 实例。

如果用 then,第一个回调抛出来的错误,第二个回调函数(then)不会捕获

1
2
3
4
5
somePromise().then(function () {
throw new Error('error');
}).catch(function (err) {
// I caught your error! :)
});

1
2
3
4
5
somePromise().then(function () {
throw new Error('error');
}, function (err) {
// I didn't catch your error! :(
});

即:当使用 then(resolveHandler, rejectHandler),rejectHandler 不会捕获在 resolveHandler 中抛出的错误。
所以最好也不用 then 的第二个回调,转而用 catch 方法。

问题: .catch() 后面如果跟着 then() 会怎么处理?
.catch() 会返回 Promise 实例,如果不抛出错误,后面跟着 .then() 会执行,如果在 catch 中抛出错误,后面的 .then() 会跳过去,直接走向最后的 .catch()。

注意四:永远传递一个函数到then方法里

看下面代码打印出什么?

1
2
3
4
5
Promise.resolve('foo')
.then(Promise.resolve('bar'))
.then(function (result) {
console.log(result);
});

结果为:foo

当then方法接收一个非函数参数时,会解释为then(null),导致之前的 Promise 的结果丢失,发生 Promise 穿透。

mdn上解释:

如果忽略针对某个状态的回调函数参数,或者提供非函数 (nonfunction) 参数,那么 then 方法将会丢失关于该状态的回调函数信息,但是并不会产生错误。如果调用 then 的 Promise 的状态(fulfill 或 rejection)发生改变,但是 then 中并没有关于这种状态的回调函数,那么 then 将创建一个没有经过回调函数处理的新 Promise 对象,这个新 Promise 只是简单地接受调用这个 then 的原 Promise 的终态作为它的终态。

所以,如果

1
2
3
4
5
6
Promise.resolve('foo')
.then(()=>{
return Promise.resolve('bar')
}).then(function (result) {
console.log(result); // bar
});

最后

最佳实践:

  • 回调函数中一定要使用 return 语句,避免丢失状态和结果。
  • 在最后一定调用 catch 方法,用来捕获整体的异常。
  • 永远传递函数给 then 方法。

参考文章:

JavaScript Promise迷你书

谈谈使用Promise时候的一些反模式

Promise Anti-patterns

Vue 动态数据绑定(五)-- 实现简单的 MVVM

Posted on 2017-10-20

任务说明地址

现在要实现最后一步了,如何在数据发生改变的时候,重新渲染 DOM。之前已经实现了数据绑定,模板渲染,现在考虑如何将这两者结合起来。
先上一张经典图来说明 vue 实现原理:
vue

Vue 数据绑定原理主要是通过数据劫持结合发布者-订阅者模式方式来实现的。每个实例对象属性都有相应的 watcher 实例对象,在渲染的时候记录属性依赖,数据更新的时候通知 watcher 重新渲染。在这主要有几个重要的概念:

  • Observer 监听器:主要对响应式对象的属性添加 getter/setter 方法,用于依赖收集和派发更新。
  • Compile 解析器:实现模板指令的解析,以及绑定更新函数。
  • Dep 订阅器:收集响应式对象的依赖关系,管理所有的观察者。
  • Watcher 观察者:收到更新通知进行视图更新。

Observer

在之前实现的 Observer 里加入订阅器 Dep,实现观察者的管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
function Observer(data) {
this.data = data;
if(Array.isArray(data)){
// 暂不考虑数组
}else{
this.makeObserver(data);
}
}

Observer.prototype.makeObserver = function(data) {
var self = this;
for (var i in data) {
if (data.hasOwnProperty(i)) {
if (typeof data[i] === 'object') {
new Observer(data[i]); // 递归添加
} else {
this.getset(i, data[i]);
}
}
}
}

Observer.prototype.getset = function (i, value) {
let val = value;
let self = this;
var dep = new Dep(); // 每个属性都有一个唯一的Dep与它对应
Object.defineProperty(this.data, i, {
configurable: true,
enumerable: true,
get: function () {
console.log('你访问了' + i);
if(Dep.target){ // 注意:这里进行收集依赖
dep.addSub(Dep.target);
}
return val;
},
set: function (newval) {
console.log('你设置了' + i + ',新的值为' + newval);
if(val === newval) {
return
}
val = newval;
if(typeof newval === 'object') {
new Observer(val);
}
dep.notify(); // 通知订阅器的所有watcher更新
}
})
}

每个属性都有一个订阅器 Dep 与其对应,用来管理该属性的所有观察者。
依赖收集是什么意思呢?对于一个模板 {{ user.name }},那么他的依赖就有 user.name 这个变量,依赖收集就是确定数据与视图的依赖关系。在触发 getter 的时候进行依赖收集,注意这里有一个重点:只有在 Dep.target 有值的时候才会进行依赖收集。也就是说,真正是因为 Vue 内部初始化数据的时候才去收集依赖,其他时候访问变量触发的 getter 就不进行收集,直接返回 value。 Dep.target是什么呢,看下面分析。
派发更新:当数据改变了,调用该属性 Dep 订阅器的 notify 方法,通知该订阅器的所有 watcher 去更新视图。例如,页面上有两处访问了 user.name,那么 user.name 的订阅器里就会有两个 watcher ,当数据更新的时候,会通知这两个 watcher 进行更新。

Dep

Dep 是依赖收集的容器,是一个 class。实际上是对 Watcher 的一种管理,记录哪些 Watcher 订阅了自己变化并在更新时进行通知。Dep 有个静态属性 target,是全局唯一的 Watcher。Dep 主要实现添加依赖 addSub 和通知 watcher 的更新 notify。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Dep () {
this.subs = []; // 观察者合集
}
Dep.target = null;

// 向subs数组添加依赖
Dep.prototype.addSub = function (sub) {
this.subs.push(sub);
};

// 通知所有订阅者更新
Dep.prototype.notify = function () {
this.subs.forEach((item) => {
item.update();
})
};

Watcher

Watcher 是观察者,在初始化的时候需要将自己添加进订阅器 Dep 中,那如何添加呢?Observer 中的 getter 进行依赖收集的时候会执行添加观察者 Watcher 的操作,所以在初始化的时候只要触发了对应的 getter 就可以,也就是去获取一次属性值。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Watcher(vm, exp, cb) {
Dep.target = this; // Watcher 初始化的时候,将Dep.target指向全局唯一的 Watcher
this.vm = vm;
this.cb = cb;
this.exp = exp;
this.get(); // 获取值的时候会触发属性的 getter,这样就将观察者加入了订阅器中,然后清空Dep.target。
Dep.target = null;
}
Watcher.prototype.get = function() {
return CompileUtil.parse(this.exp)(this.vm.data);
}
Watcher.prototype.update = function() { // 观察者进行更新
this.newVal = this.get(); // 获取新值,这个时候已经不会再将观察者加入订阅器中了,因为 Dep.target 已经为 null。
this.cb(this.newVal)
}

在初始化的时候Dep.target指向唯一的观察者,触发属性的 getter 添加监听,最后将 Dep.target 清空。
这儿有个 CompileUtil.parse 为了解析深层属性,返回最终的值。

1
2
3
4
5
6
7
8
9
10
11
CompileUtil.parse = function(exp) { // 解析多层路径
if (/[^\w.$]/.test(exp)) return;
var exps = exp.split('.');
return function(obj) {
for (var i = 0, len = exps.length; i < len; i++) {
if (!obj) return;
obj = obj[exps[i]];
}
return obj;
}
}

Compile

Compile解析器主要工作是两点

  1. 解析模板指令,初始化模板视图。
  2. 绑定模板指令节点与更新方法,初始化订阅器。

上节编译器 Compile 只是实现了第一点,现在要加上初始化的时候绑定更新的功能,在初始化视图的时候,初始化一个 watcher,此时将此 watcher 添加进该属性的 Dep 中,同时绑定更新函数,这样在数据更新的时候,调用此更新函数。

1
2
3
4
5
6
7
8
9
10
11
12
13

Compile.prototype.compileText = function (node, exp) {
// 匹配 {{ }}替换
let self = this;
var newVal = CompileUtil.parse(exp)(this.vm.data);
this.updateText(node, newVal); // 初始化视图
new Watcher(this.vm, exp, function (value) { // 生成订阅器并绑定更新函数
self.updateText(node, value);
});
}
Compile.prototype.updateText = function (node, value) { // 只实现简单的替换
node.textContent = typeof value == 'undefined' ? '' : value;
}

最后,实现 vue 的初始化,根据上图,需要绑定 Observe 和 Compile 。

1
2
3
4
5
6
function Vue(obj) {
this.el = obj.el;
this.data = obj.data;
new Observer(this.data);
new Compile(this.el, this);
}

实现代码

效果如下:

mvvm

Vue 动态数据绑定(四)-- 解析器 Compile

Posted on 2017-09-10

任务说明地址

将页面中代码片段

1
2
3
4
5
<!-- 页面中原本的 html 模板片段 -->
<div id="app">
<p>姓名:{{user.name}}</p>
<p>年龄:{{user.age}}</p>
</div>

渲染成实际效果

1
2
3
4
5
<!-- 最终在页面中渲染出来的结果 -->
<div id="app">
<p>姓名:youngwind</p>
<p>年龄:25</p>
</div>

所以问题的关键:怎么实现一个解析器进行模板渲染?

简单的思路:深度遍历DOM模板,找到{{}}进行解析替换。
按照此思路实现一个简单的版本:
代码 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

function Compile(el, vm) {
this.el = el;
this.vm = vm;
this.render();
}
Compile.prototype.render = function () {
var el = document.querySelector(this.el);
this.compile(el);
}
Compile.prototype.compile = function (el) {
// 递归遍历所有的dom子元素 找到 {{}} 进行替换

var childNodes = el.childNodes;
childNodes.forEach((item) => {

if(item.nodeType === 1) { // 元素节点
this.compile(item);
} else if (item.nodeType === 3) { // 文本内容
console.log(item.nodeValue);
var value = item.nodeValue.trim();
var reg = /{{(.*?)}}/;
if (value && reg.test(value)) {
this.compileText(item, reg.exec(value)[1]);
}
}
})
}
Compile.prototype.compileText = function(node, exp) {
var newVal = this.parse(exp.trim())(this.vm.data);
node.textContent = typeof newVal == 'undefined' ? '' : newVal;
}
Compile.prototype.parse = function (exp) { // 多层路径进行解析
if (/[^\w.$]/.test(exp)) return;

var exps = exp.split('.');
return function(obj) {
for (var i = 0, len = exps.length; i < len; i++) {
if (!obj) return;
obj = obj[exps[i]];
}
return obj;
}
}

上述实现方法是找到DOM直接替换,在遍历解析的时候有多次操作DOM节点,为了提高性能和效率,考虑用文档碎片fragment的方式。初始化时将节点转换成 fragment,解析完成再整体添加到真实的DOM节点中。

代码 DEMO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Compile(el, vm) {
this.el = el;
this.vm = vm;
this.fragement = this.nodeToFragment(document.querySelector(this.el));
this.compile(this.fragement);
document.querySelector(this.el).appendChild(this.fragement);
}
Compile.prototype.nodeToFragment = function(el) {
var fragment = document.createDocumentFragment();
var child = el.firstChild;
while (child) {
fragment.appendChild(child); // 将Dom元素移入fragment中 注意: append的时候 原来的dom会删掉挂载在文档碎片上
child = el.firstChild;
}
return fragment;
}

1234

ruirui

37 posts
7 tags
© 2022 ruirui
Powered by Hexo
|
Theme — NexT.Muse v6.0.0